package com.google.code.stalkernet.service;

import java.net.SocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import com.google.code.stalkernet.advertisement.Advertisement;
import com.google.code.stalkernet.advertisement.AdvertisementUtils;
import com.google.code.stalkernet.advertisements.Advertisements;
import com.google.code.stalkernet.async.Async.Callback;
import com.google.code.stalkernet.async.Async.Handle;
import com.google.code.stalkernet.async.factory.AsyncFactory;
import com.google.code.stalkernet.node.Node;
import com.google.code.stalkernet.node.NodeID;
import com.google.code.stalkernet.node.event.NodeEvent;
import com.google.code.stalkernet.node.event.NodeListener;
import com.google.code.stalkernet.peer.Peer;
import com.google.code.stalkernet.peer.event.PeerEvent;
import com.google.code.stalkernet.peer.event.PeerListener;
import com.google.code.stalkernet.reactor.Reactor;
import com.google.code.stalkernet.reactor.Reactor.ScheduledTaskHandle;

/**
 * Server that automatically connects to advertised peers and readvetrises them
 * if connected successfully.
 *
 * @author Mikhail Vladimirov
 */
public class AdvertisementConnectorService implements Service
{
    private final static Logger logger = Logger.getLogger (
        AdvertisementConnectorService.class.getName ());

    private final MyNodeListener nodeListener = new MyNodeListener ();

    private final Map <SocketAddress, SocketAddressWrapper> socketAddresses =
        new HashMap <> ();

    private final Reactor reactor;
    private final Advertisements advertisements;
    private final AsyncFactory <? super SocketAddress, ? extends Peer>
        peerFactory;
    private final Node node;

    /**
     * Create new advertisement connector service that uses given executor
     * and node.
     *
     * @param node {@link Node} to use
     */
    public AdvertisementConnectorService (
        Reactor reactor,
        Advertisements advertisements,
        AsyncFactory <? super SocketAddress, ? extends Peer> peerFactory,
        Node node)
    {
        if (reactor == null)
            throw new IllegalArgumentException ("Reactor is null");

        if (advertisements == null)
            throw new IllegalArgumentException (
                "Advertisements is null");

        if (peerFactory == null)
            throw new IllegalArgumentException ("Peer factory is null");

        if (node == null)
            throw new IllegalArgumentException ("Node is null");

        this.reactor = reactor;
        this.advertisements = advertisements;
        this.peerFactory = peerFactory;
        this.node = node;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void start ()
    {
        node.addNodeListener (nodeListener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop ()
    {
        node.removeNodeListener (nodeListener);
    }

    /**
     * Connect to given socket address.
     *
     * @param socketAddress {@link SocketAddress} to connect to
     */
    public void connect (SocketAddress socketAddress)
    {
        if (socketAddress == null)
            throw new IllegalArgumentException ("Socket address is null");

        if (!socketAddresses.containsKey (socketAddress))
        {
            SocketAddressWrapper socketAddressWrapper =
                new SocketAddressWrapper (socketAddress);
            socketAddresses.put (socketAddress, socketAddressWrapper);
            socketAddressWrapper.start ();
        }
    }

    private class MyNodeListener implements NodeListener
    {
        @Override
        public void peerAdded (NodeEvent nodeEvent)
        {
            if (nodeEvent == null)
                throw new IllegalArgumentException ("Node event is null");

            // Do nothing
        }

        @Override
        public void peerRemoved (NodeEvent nodeEvent)
        {
            if (nodeEvent == null)
                throw new IllegalArgumentException ("Node event is null");

            // Do nothing
        }

        @Override
        public void advertisementReceived (NodeEvent nodeEvent)
        {
            if (nodeEvent == null)
                throw new IllegalArgumentException ("Node event is null");

            connect (nodeEvent.getAdvertisement ().getSocketAddress ());
        }
    }

    private class SocketAddressWrapper
        implements Callback <Peer>, Runnable, PeerListener
    {
        private final SocketAddress socketAddress;

        private Handle produceHandle = null;
        private ScheduledTaskHandle retryHandle = null;

        private Advertisement advertisement = null;

        public SocketAddressWrapper (SocketAddress socketAddress)
        {
            if (socketAddress == null)
                throw new IllegalArgumentException ("Socet address is null");

            this.socketAddress = socketAddress;
        }

        public void start ()
        {
            connect ();
        }

        public void stop ()
        {
            if (retryHandle != null)
            {
                retryHandle.cancel ();
                retryHandle = null;
            }
        }

        private void connect ()
        {
//            logger.info ("Connecting to " + socketAddress);

            produceHandle = peerFactory.produce (socketAddress, this);
        }

        private void retry ()
        {
            retryHandle = reactor.schedule (
                this, System.currentTimeMillis () + 1000L);
        }

        @Override // Callback <Peer>
        public void onSuccess (Peer peer)
        {
            if (peer == null)
                throw new IllegalArgumentException ("Peer is null");

            produceHandle = null;

            NodeID remoteNodeID = peer.getRemoteNodeID ();

            Advertisement advertisement =
                AdvertisementUtils.createAdvertisement (
                    socketAddress, remoteNodeID);

            if (!advertisement.equals (this.advertisement))
            {
                if (this.advertisement != null)
                    advertisements.removeAdvertisement (
                        this.advertisement);

                advertisements.addAdvertisement (advertisement);

                this.advertisement = advertisement;
            }

            peer.addPeerListener (this);

            node.addPeer (peer);
        }

        @Override // Callback <Peer>
        public void onError (Throwable error)
        {
            produceHandle = null;

            if (this.advertisement != null)
            {
                advertisements.removeAdvertisement (
                    this.advertisement);

                advertisement = null;
            }

            retry ();
        }

        @Override // Runnable
        public void run ()
        {
            retryHandle = null;

            connect ();
        }

        @Override
        public void messageReceived (PeerEvent peerEvent)
        {
            if (peerEvent == null)
                throw new IllegalArgumentException ("Peer event is null");

            // Do nothing
        }

        @Override
        public void disconnected (PeerEvent peerEvent)
        {
            if (peerEvent == null)
                throw new IllegalArgumentException ("Peer event is null");

            retry ();
        }
   }
}
